Setting Slack Status with PomoDoneApp using Google App Scripts

PomoDoneApp is an app that I use on a regular basis to keep myself on task, and monitor my productivity. The thing that really sets it apart from other similar apps is all of the built-in integrations with many of the popular productivity tools like Trello, Todoist, Slack, JIRA and so on.

The built-in slack integration uses a public Slack bot, which has the ability to post in slack channels, but is unable to set a user's status. Personally, when I am working, I like to set my status to let my co-workers know not to bother me, or at least let them know why I am not replying to their messages. So, when I originally started using the app, I used their integration with Zapier to make zaps that would change my status automatically. This is very easy to set up and works great, but if, like me, you don't want to spend 25 dollars a month for a Zapier membership, then you are limited to 100 "tasks" per month, which really isn't enough if you are changing your status many times throughout the day.

So I went searching for free solutions. I figured that Zapier must be using some API to do what it is doing, so I should be able to use that same API with something that I have developed myself. The PomoDoneApp API has basically know documentation, so I had to reach out to the PomoDoneApp team to ask them how to use their API. I am using Google API scripts in this tutorial, but if you don't want to use Google, this can easily be adapted to use something home-grown. I am planning on doing that in the future, but the Google solution works well enough for the time being.

The Google scripts that I am using here are adapted from this google doc that was sent to me by Alex Mauzon of the PomoDoneApp Team, so kudos to him for helping me get this up and running.

Creating a slack bot

The first thing that needs to be done, is you need to create a slack bot that is capable of changing your status. You can do this by going to the Apps dashboard on api.slack.com. There you can create a new app. You then just need to go to the OAuth & Permissions section in the left sidebar and add users.profile:write scope in the User Token Scopes section under Scopes. Then you can scroll up to the OAuth Tokens for Your Workscape section, install the app, and get the User OAuth Token. This token should begin with xoxp. Once you have that token, you are ready to create your scripts.

Creating the Google App Scripts scripts

Go to script.google.com and make sure to sign in with your Google Account. Then create a new project. The project will be created with a blank .gs script called Code.gs:

function myFunction() {

}

The slack status script

Change myFunction to testSetStatus and create another function called setStatus that looks like the one below.

const APITOKEN = 'xoxp-my-token';

function testSetStatus() {
  setStatus('test status', ':man-shrugging', 0);
}

function setStatus(status, emoji, timeEnd) {
  var header = {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer " + APITOKEN,
  };

  var payload = {
    "profile": {
      "status_text": status,
      "status_emoji": emoji,
      "status_expiration": timeEnd
    }
  };

  var options = {
    'method' : 'post',
    'headers' : header,
    'muteHttpExceptions': false,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = "https://cyclicarx.slack.com/api/users.profile.set";
  response = UrlFetchApp.fetch(url2fetch, options);
  console.log(response.getContentText())
}

You can then open the Execution log, select testSetStatus from the menu bar and hit Run. If you set up your slack bot correctly, you should see your slack status change to "test status". If the request is unsuccessful, there will be a console message showing the response data which should give you some idea why it didn't work.

If this was successful, it's time to set up the PomoDoneApp side of things. First add another function to Code.gs called doPost:

/*
 * Function run when pomodoneapp posts event to google script
 */
function doPost(e) {
  if(e.postData){
    var myData = JSON.parse(e.postData.contents);
  }

  if(myData){
    var pdEvent = myData.eventType; // timerStart, timerStop, cardDone
    var pdTaskName = myData.name; // "Your Task Name",
    var pdTimerSize = myData.minutes; // Duration of the timer, e.g. 5
  }

  if(pdEvent.toLowerCase() == 'timerstop'){
    // Run on timerstop event
    var response = setStatus('', '', 0);
  }

  if(pdEvent.toLowerCase() == 'timerstart'){
    // Run on timerstart event
    var timeEnd = Math.floor(Date.now()/1000) + (pdTimerSize*60);
    var response = setStatus(
      'Working on ' + pdTaskName + ' for the next ' + pdTimerSize + ' minutes.', 
      ':thinking_face:', timeEnd
    );
  }
}

This is the function that will be run by the PomoDoneApp. When the timer is stopped, it sends a blank status update, effectively clearing the status, and when the timer starts, it uses the name of the event and the time in minutes to generate an expiry for the status.

The PomoDoneApp Registration Script

Now that we have a scipt that can change our slack status, we need to tell PomoDoneApp to use it. Press the + symbol in the Files section of the left sidebar and select Script. Call it whatever you want. Mine is called pgHookReg.gs. You will be presented with a new file with the same myFunction function in it.

We'll need 4 constants for this file:

const MYAPIKEY = 'my-pomodoneapp-API-key';
const MYINTEGRATIONNAME = 'gsheet';
const EVENTTOADD = 'timerStart';
const DEPLOYMENTID = "my-deploy-id";

The DEPLOYMENTID can be filled in after we deploy the script. MYAPIKEY can be found in your PomoDoneApp account. You have to go to My Settings and look for Your API Key section.

Next we need a function for registering the webhook with pomodoneapp:

function registerWebHook() {
  var header = {"Content-Type": "application/json"};

  var payload = {
    "subscription_url": "registered By Google Script",
    "target_url": "https://script.google.com/macros/s/" + DEPLOYMENTID + "/exec",
    "event": EVENTTOADD
  };

  var options = {
    'method' : 'POST',
    'header' : header,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = 'https://my.pomodoneapp.com/integration/authorize/?integration=' + 
    MYINTEGRATIONNAME + '&params[api_key]=' + MYAPIKEY;
  response = UrlFetchApp.fetch(url2fetch, options);
}

We'll also want a function to remove the web hook if we want to change something in the future:

function removeAllWebHooks() {
  var header = {"Content-Type": "application/json"};
  var payload = {
    "target_url": "https://script.google.com/macros/s/" + DEPLOYMENTID + "/exec"
  }

  var options = {
    'method' : 'POST',
    'header' : header,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = 'https://my.pomodoneapp.com/integration/remove/?integration=' + 
    MYINTEGRATIONNAME + '&params[api_key]='+MYAPIKEY;
  response = UrlFetchApp.fetch(url2fetch, options);
}

Deploying the scripts

At this point your scripts should look like this: Code.gs

const APITOKEN = 'xoxp-my-token';

function testSetStatus() {
  setStatus('test status', ':man-shrugging', 0);
}

function setStatus(status, emoji, timeEnd) {
  var header = {
    "Content-Type": "application/json; charset=utf-8",
    "Authorization": "Bearer " + APITOKEN,
  };

  var payload = {
    "profile": {
      "status_text": status,
      "status_emoji": emoji,
      "status_expiration": timeEnd
    }
  };

  var options = {
    'method' : 'post',
    'headers' : header,
    'muteHttpExceptions': false,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = "https://cyclicarx.slack.com/api/users.profile.set";
  response = UrlFetchApp.fetch(url2fetch, options);
  console.log(response.getContentText())
}

/*
 * Function run when pomodoneapp posts event to google script
 */
function doPost(e) {
  if(e.postData){
    var myData = JSON.parse(e.postData.contents);
  }

  if(myData){
    var pdEvent = myData.eventType; // timerStart, timerStop, cardDone
    var pdTaskName = myData.name; // "Your Task Name",
    var pdTimerSize = myData.minutes; // Duration of the timer, e.g. 5
  }

  if(pdEvent.toLowerCase() == 'timerstop'){
    // Run on timerstop event
    var response = setStatus('', '', 0);
  }

  if(pdEvent.toLowerCase() == 'timerstart'){
    // Run on timerstart event
    var timeEnd = Math.floor(Date.now()/1000) + (pdTimerSize*60);
    var response = setStatus(
      'Working on ' + pdTaskName + ' for the next ' + pdTimerSize + ' minutes.', 
      ':thinking_face:', timeEnd
    54);
  }
}

pgHookReg.gs

const MYAPIKEY = 'my-pomodoneapp-API-key';
const MYINTEGRATIONNAME = 'gsheet';
const EVENTTOADD = 'timerStart';
const DEPLOYMENTID = "my-deploy-id";

function registerWebHook() {
  var header = {"Content-Type": "application/json"};

  var payload = {
    "subscription_url": "registered By Google Script",
    "target_url": "https://script.google.com/macros/s/" + DEPLOYMENTID + "/exec",
    "event": EVENTTOADD
  };

  var options = {
    'method' : 'POST',
    'header' : header,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = 'https://my.pomodoneapp.com/integration/authorize/?integration=' + 
    MYINTEGRATIONNAME + '&params[api_key]=' + MYAPIKEY;
  response = UrlFetchApp.fetch(url2fetch, options);
}

function removeAllWebHooks() {
  var header = {"Content-Type": "application/json"};
  var payload = {
    "target_url": "https://script.google.com/macros/s/" + DEPLOYMENTID + "/exec"
  }

  var options = {
    'method' : 'POST',
    'header' : header,
    'payload': JSON.stringify(payload)
  };

  var url2fetch = 'https://my.pomodoneapp.com/integration/remove/?integration=' + 
    MYINTEGRATIONNAME + '&params[api_key]='+MYAPIKEY;
  response = UrlFetchApp.fetch(url2fetch, options);
}

Now we need to deploy. In the top right of the editor window, you should see a big blue Deploy button. Click that and select New deployment. The type should be Web app. Give it any description that you'd like and hit Deploy. Once it is deployed you will be given a deployment ID that you can put into the DEPLOYMENTID parameter in pgHookReg.gs. All you need to do now is open Execution log, select registerWebHook from the dropdown at the top bar and hit Run. If there is no error, then the webhook should now be registered. Test that its working by opening the pomodoneapp and running a timer. You should then see your status change.

Now we have the scripts deployed and the 'timerStart' event registered, but we still need to add a 'timerStop' hook. Change EVENTTOADD to 'timerStop' and run registerWebHook again. Then stop the timer in your pomodone app and you should see your status clear.

Edit: Home-grown Solution

Date: 20220313

Since creating the above scripts, I set out to find a solution that I could host at home instead of relying on Google servers to run it. I ended up creating this Flask app, which is essentially a python rendition of the above scripts. I run this at home on a Raspi 4 now. It can easily be run on any cloud VM as well, but in that case, it's probably cheaper to just continue using the Google script. If you'd like to use this app instead, you can follow the README to get it up and running.